package dungeonElements;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import temperature.Temperature;
import be.kuleuven.cs.som.annotate.Raw;
import dungeonTools.Direction;

/**
 * A class of rocks (squares full of rocks). A rock can not contain anything
 * and nothing can move through a rock. A rock is fully surrounded by walls 
 * without doors and always have zero humidity and the temperature is the 
 * average temperature of the surrounding squares (zero if no squares are bordering).
 * 
 * @invar Each rock must have walls in every direction.
 * 		| for each direction in Direction:
 * 		|	this.getBorder(direction) instanceof Wall
 * @invar Each rock must have zero humidity.
 * 		| this.getHumidity() == 0.0d
 * @invar Each rock must have the average temperature of it's surrounding squares 
 * 		or zero if there arn't any bordering squares.
 * 		| canHaveAsTemperature(getTemperature())
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 */
public class Rock extends Square {

	/**
	 * Creates a new rock with the following attributes:
	 * Temperature - Temperature.CELSIUS_0 (setTemperature())
	 * Humidity - 0.0d
	 * Borders - walls in every direction
	 * Slippery Surface - DEFAULT: false
	 * Terminated - DEFAULT: false
	 * 
	 * @effect This new rock is initialized as a square with the attributes defined above.
	 * 		| super(Temperature.CELSIUS_0, 0.0d, true, true, true, true, true, true)
	 * @effect The temperature is set to the averages temperature of the bordering squares.
	 * 		| setTemperature()
	 */
	@Raw
	public Rock() {
		super(Temperature.CELSIUS_0, 0.0d, true, true, true, true, true, true);
		setTemperature();
		initializationDone = true;
	}
	
	/**
	 * Boolean used in the initialization of a rock. Will be set to true if initialization is done.
	 */
	private boolean initializationDone = false;
	
	
	/**
	 * Check whether this rock can have the given temperature as its temperature.
	 * 
	 * @param 	temperature
	 * 			The temperature to check.
	 * @return 	False if the given temperature isn't a possible
	 * 			temperature for this square. The new temperature should be 
	 * 			between the maximum and minimum temperature of this square.
	 * 			| if( !(this.matchesTemperatureMaxTemperature(temperature) 
	 * 			|		&& this.matchesTemperatureMinTemperature(temperature)) )
	 * 			|	then result == false
	 * @return False if the new temperature isn't the average temperature of 
	 * 			the surrounding squares.
	 * 			| if(!temperature.equals(getAverageTemperatureOfBorderingSquares()))
	 * 			|	then result == false
	 * @throws IllegalArgumentException
	 * 			temperature must be effective
	 * 			|temperature == null
	 */
	@Override @Raw
	public boolean canHaveAsTemperature(Temperature temperature) throws IllegalArgumentException{
		if(temperature == null)
			throw new IllegalArgumentException("Non-effective temperature");
		if(initializationDone == false)
			return true;
		if(!super.canHaveAsTemperature(temperature)) 
			return false;
		if(!temperature.equals(getAverageTemperatureOfBorderingSquares()))
			return false;
		
		return true;
	}
	
	/**
	 * Set the temperature for this rock to the average temperature of the bordering squares.
	 * 
	 * @post The new temperature for this rock is equal to the average temperature 
	 * 		of the bordering squares.
	 * 		| new.getTemperature().equals(getAverageTemperatureOfBorderingSquares())
	 * @effect Set the temperature to the average temperature of the bordering squares.
	 * 		| super.setTemperature(getAverageTemperatureOfBorderingSquares())
	 */
	@Raw 
	public void setTemperature(){
		if(initializationDone == false)
			return;
		for(Rock rock : getAllBorderingRocks())
			rock.setTemperature(getAverageTemperatureOfBorderingSquares());
	}
	
	/**
	 * Get the average temperature of the bordering squares. If a rock borders to other 
	 * rocks, the average temperature is calculated with all the bordering squares that
	 * aren't rocks form all the bordering rocks. So bordering rocks always have the 
	 * same temperature. 
	 * 
	 * @return If this rock doesn't have bordering squares a temperature 
	 * 		of zero degree celsius is returned. If this rock has bordering squares 
	 * 		the average temperature of the bordering squares is returned.
	 */
	@Raw
	public Temperature getAverageTemperatureOfBorderingSquares(){
		BigDecimal averageNumeral = BigDecimal.ZERO;
		List<Temperature> temperatures = new ArrayList<Temperature>();
		Set<Rock> allBorderingRocks = getAllBorderingRocks();
		for(Rock borderingRocks : allBorderingRocks)
			temperatures.addAll(borderingRocks.getTemperaturesOfBorderingSquares());
		if(temperatures.isEmpty())
			return Temperature.CELSIUS_0;
		for(Temperature temperature: temperatures)
			averageNumeral = averageNumeral.add(temperature.toCelsius().getNumeral());
		averageNumeral = averageNumeral.divide(BigDecimal.valueOf(temperatures.size()), Temperature.TEMPERATURE_PRECISION, RoundingMode.HALF_EVEN);
		Temperature averageTemperature = new Temperature(averageNumeral);
		return averageTemperature;
	}
	
	/**
	 * Get a list of temperatures from the bordering squares that aren't Rocks.
	 * The list will be empty if
	 * 
	 * @return A list of the temperatures of the bordering squares that aren't Rocks.
	 * 		An empty list is returned of this rock has no bordering squares that 
	 * 		aren't Rocks.
	 * 		| Let temperatures be a list of Temperatures in
	 * 		|	for each direction in Direction:
	 * 		|		if( getBorder(direction).hasSquareInDirection(direction) 
	 * 		|				&& !(getBorder(direction).getSquareInDirection(direction) instanceof Rock) )
	 * 		|			temperatures.add(getBorder(direction).getSquareInDirection(direction).getTemperature())
	 * 		|	result == temperatures
	 */
	@Raw
	private List<Temperature> getTemperaturesOfBorderingSquares(){
		ArrayList<Temperature> temperatures = new ArrayList<Temperature>();
		for(Direction direction : Direction.values()){
			Border border = getBorder(direction);
			if( border.hasSquareInDirection(direction) 
					&& !(border.getSquareInDirection(direction) instanceof Rock) )
				temperatures.add(border.getSquareInDirection(direction).getTemperature());
		}
		return temperatures;
	}
	
	/**
	 * Get a set of Rocks witch are bordering directly to this Rock.
	 * 
	 * @return A set of Rocks witch are directly bordering to this square.
	 * 		An empty set will be returned if this rock has no bordering rocks.
	 * 		| Let rocks be a list of Rocks in
	 * 		|	for each direction in Direction:
	 * 		|		if( getBorder(direction).hasSquareInDirection(direction)
	 * 		|				&& (getBorder(direction).getSquareInDirection(direction) instanceof Rock) )
	 * 		|			rocks.add((Rock) getBorder(direction).getSquareInDirection(direction))
	 * 		|	result == rocks
	 */
	@Raw
	private Set<Rock> getBorderingRocks(){
		HashSet<Rock> rocks = new HashSet<Rock>();
		for(Direction direction : Direction.values()){
			Border border = getBorder(direction);
			if( border.hasSquareInDirection(direction) 
					&& (border.getSquareInDirection(direction) instanceof Rock) )
				rocks.add((Rock) border.getSquareInDirection(direction));
		}
		return rocks;
	}
	
	/**
	 * Get a set of Rocks witch are bordering directly or indirectly to this Rock.
	 * 
	 * @return A set of Rocks witch are bordering to this square directly or Rocks witch are connected 
	 * through other Rocks.
	 */
	@Raw
	private Set<Rock> getAllBorderingRocks(){
		assert queuedRocks.isEmpty();
		assert detectedRocks.isEmpty();
		queuedRocks.add(this);
		while(queuedRocks.size()>0){
			HashSet<Rock> setToQueue = new HashSet<Rock>();
			for(Rock queuedRock : queuedRocks){
				setToQueue.addAll(rocksToQueue(queuedRock.getBorderingRocks() ) );
			}
			detectedRocks.addAll(queuedRocks);
			queuedRocks = setToQueue;
		}
		Set<Rock> allBorderingRocks = new HashSet<Rock>(detectedRocks);
		queuedRocks.clear();
		detectedRocks.clear();
		return allBorderingRocks;
	}
	
	/**
	 * Filter the rocks that aren't already detected.
	 * 
	 * @param rocks
	 * 		The set of rocks to be checked.
	 * @return A set of bordering rock that aren't detected.
	 * 		| for each rock in rocks
	 * 		|	if((!detectedRocks.contains(rock)) )
	 *		|		set.add(rock);
	 */
	@Raw
	private Set<Rock> rocksToQueue(Set<Rock> rocks){
		Iterator<Rock> iterator = rocks.iterator();
		Set<Rock> set = new HashSet<Rock>();
		while(iterator.hasNext()){
			Rock rock = iterator.next();
			if((!detectedRocks.contains(rock)) )
				set.add(rock);
		}
		return set;
	}
	
	/**
	 * A set with detected bordering rocks.
	 */
	private static HashSet<Rock> detectedRocks = new HashSet<Rock>();
	
	/**
	 * A set with detected bordering squares, which need to be checked for bordering squares.
	 */
	private static HashSet<Rock> queuedRocks = new HashSet<Rock>();
	
	
	/**
	 * Check whether this rock can have the given border in the given direction 
	 * as its border. A non-terminated rock can only have walls as its borders.
	 * 
	 * @param border
	 * 		The border to check
	 * @param direction
	 * 		The direction to check
	 * @return If a square can't have this border as a border the result will be false.
	 * 		| if(!super.canHaveAsBorderInDirection(border, direction))
	 * 		|	then result == false
	 * @return False if this rock is not terminated and the border is not a wall
	 * 		| if( !isTerminated() &&  !(border instanceof Wall) )
	 *		|	then result == false
	 */
	@Override @Raw
	public boolean canHaveAsBorderInDirection(@Raw Border border, Direction direction){
		if(!super.canHaveAsBorderInDirection(border, direction))
			return false;
		if( !isTerminated() && !(border instanceof Wall) )
			return false;
		return true;
	}
	
	/**
	 * Check whether this rock has proper borders.
	 * 
	 * @return False if for some direction this rock does not have a wall.
	 * 		| for each direction in Direction:
	 * 		|	if(getBorder(direction) instanceof Wall)
	 * 		|		then result == false
	 * @return False if in some direction this square has an border that isn't proper set 
	 * 		or is terminated.
	 * 		| for each direction in Direction:
	 *		|	if( (borders.get(direction) != null) 
	 *		|		&& (borders.get(direction).getSquareInDirection(direction.getOppositeDirection()) != this) )
	 *		|	then result == false
	 * 		|	if( (borders.get(direction) != null) && (borders.get(direction).isTerminated()) )
	 * 		|		then result == false
	 */
	@Override @Raw
	public boolean hasProperBorders(){
		for(Direction direction : Direction.values()){
			if( !(getBorder(direction) instanceof Wall) )
				return false;
			if( !hasProperBorderInDirection(direction) )
				return false;
		}
		return super.hasProperBorders();
	}
	
	
	/**
	 * Checks if this Rock can have the given humidity.
	 * 
	 * @param humidity 
	 * 		The humidity value to be checked
	 * @return False if the humidity is not zero.
	 * 		| if(humidity != 0.0d)
	 * 		|	then result == false
	 */
	@Override
	public boolean canHaveAsHumidity(double humidity){
		if(humidity != 0.0d)
			return false;
		return true;
	}
	
}
